package com.vzaar.transport.httpclient3;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Map.Entry;

import oauth.signpost.OAuthConsumer;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import oauth.signpost.signature.SignatureMethod;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.protocol.Protocol;

import com.vzaar.UploadProgressCallback;
import com.vzaar.VzaarException;
import com.vzaar.transport.VzaarTransport;
import com.vzaar.transport.VzaarTransportResponse;

/**
 *	VzaarTransport implementation using commons HttpClient 3.1.
 * 
 * @author Marc G. Smith
 */
class HttpClientTransport implements VzaarTransport {
	
	///////////////////////////////////////////////////////////////////////////
	// Private Members ////////////////////////////////////////////////////////
	///////////////////////////////////////////////////////////////////////////

	private HttpClient client;
	private HttpState state;
	private String apiUrl;
	private OAuthConsumer consumer;
	private OutputStream debugOut;
	
	///////////////////////////////////////////////////////////////////////////
	// Methods ////////////////////////////////////////////////////////////////
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Constructor.
	 */
	HttpClientTransport() {
		this.client = new HttpClient();
		this.state = new HttpState();
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Send any get request. If OAuth credentials have been added then OAuth
	 * authentication headers are sent.
	 * 
	 * @param uri the uri of the request which is appended to the API URL
	 * @param parameters request parameters.
	 */
	public VzaarTransportResponse sendGetRequest(
		String uri, Map<String, String> parameters) 
		throws VzaarException
	{
		try {
			String url = constructGetUrl(uri, parameters);
			if(debugOut != null) {
				debugOut.write(("\n>> Request URL = " + url + "\n").getBytes());
				debugOut.write((">> Request Method = GET\n").getBytes());
			}
			GetMethod method = new GetMethod(url);
			return excuteMethod(method);
		} 
		catch (Exception e) {
			throw new VzaarException(e.getMessage());
		}
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Send any post request. If OAuth credentials have been added then OAuth
	 * authentication headers are sent.
	 * 
	 * @param uri the method URI
	 * @param parameters request parameters
	 * @return the call response 
	 */
	public VzaarTransportResponse sendPostRequest(
		String uri, Map<String, String> parameters) 
		throws VzaarException
	{
		try {
			String url = apiUrl + uri;
			if(debugOut != null) {
				debugOut.write(("\n>> Request URL = " + url + "\n").getBytes());
				debugOut.write((">> Request Method = POST\n").getBytes());
				debugOut.write((">> Request Parameters = " + 
					parameters + "\n").getBytes());
			}

			PostMethod method = new PostMethod(url);
			addPostParameters(method, parameters);
			return excuteMethod(method);
		} 
		catch (Exception e) {
			throw new VzaarException(e.getMessage());
		}
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Send a post request with an XML body. If OAuth credentials have been 
	 * added then OAuth authentication headers are sent.
	 * 
	 * @param uri the method URI
	 * @param xml the XML content for the body of the request
	 * @return the call response 
	 */
	public VzaarTransportResponse sendPostXmlRequest(
		String uri, String xml) 
		throws VzaarException
	{
		try {
			String url = apiUrl + uri;
			
			if(debugOut != null) {
				debugOut.write(("\n>> Request URL = " + url + "\n").getBytes());
				debugOut.write((">> Request Method = POST\n").getBytes());
				debugOut.write((">> Request Body = \n>> " + 
					xml.replaceAll("\n", "\n>> ") + "\n").getBytes());
			}

			PostMethod method = new PostMethod(url);
			method.setRequestHeader("Content-type", "text/xml");
			method.setRequestEntity(new StringRequestEntity(
				xml, "text/xml", "UTF-8"));
			return excuteMethod(method);
		} 
		catch (Exception e) {
			throw new VzaarException(e.getMessage());
		}
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Upload a file to S3. This method doesn't use OAuth. If the callback is
	 * not null then the method should send progress reports to the callback.
	 * There is no need to call the error or complete calls on the callback
	 * as these are done by the Vzaar calling class.
	 *
	 * @param url The full url to the S3 bucket
	 * @param paramets the query parameters to send
	 * @param file the file to upload
	 * @param callback the callback to register for updates or null if not
	 *  required
	 * @return the call response 
	 */
	public VzaarTransportResponse uploadToS3(
		String url, Map<String, String> parameters, File file,
		UploadProgressCallback callback) 
		throws VzaarException 
	{
		try {
			PostMethod method = new PostMethod(url);
			Part[] parts = new Part[parameters.size() + 1];

			int i = 0;
			for(Entry<String, String> entry : parameters.entrySet()) {
				String value = entry.getValue();
				parts[i++] = new StringPart(entry.getKey(), value);  
			}
			
			parts[i] = callback == null ?
				new FilePart("file", file) :
				new ProgressFilePart("file", file, callback);

			if(debugOut != null) {
				debugOut.write(("\n>> Request URL = " + url + "\n").getBytes());
				debugOut.write((">> Request Method = POST\n").getBytes());
				debugOut.write((">> Request Parameters = " + 
						parameters + "\n").getBytes());
				debugOut.write((">> Request File = " + 
						file.getAbsolutePath() + "\n").getBytes());
			}

			method.setRequestEntity(
				new MultipartRequestEntity(
					parts, method.getParams()));

			client.executeMethod(method);
			
			String response = method.getResponseBodyAsString();
			if(response == null) response = "";
			
			if(debugOut != null) {
				debugOut.write(("<< Response Code = " + 
					method.getStatusCode() + "\n").getBytes());
				debugOut.write(("<< Response Line = " + 
					method.getStatusLine().getReasonPhrase() + "\n").getBytes());
				debugOut.write(("<< Response Body = \n<< " +
					response.replaceAll("\n", "\n<< ") + "\n").getBytes());
			}
			
			return new VzaarTransportResponse(
				method.getStatusCode(),
				method.getStatusLine().getReasonPhrase(),
				new ByteArrayInputStream(response.getBytes()));
		} 
		catch (Exception e) {
			throw new VzaarException(e.getMessage(), e);
		}
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Set the base API URL, such as http://vzaar.com/api/
	 * 
	 * @param url the base API URL
	 */
	public void setUrl(String url) {
		if(!url.endsWith("/")) {
			url += "/";
		}
		
		if(!url.endsWith("api/")) {
			url += "api/";
		}
		
		this.apiUrl = url;
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Set the OAuth 2 party tokens. If these are supplied then all calls to
	 * <a href="http://vzaar.com/">vzaar.com</a> will send OAuth credentials.
	 * 
	 * @param accessToken the users login name
	 * @param accessTokenSecret the secret token supplied by vzaar
	 */
	public void setOAuthTokens(
		String accessToken, String accessTokenSecret) 
	{
		if(accessToken == null) {
			consumer = null;
		}
		else {
			consumer = new CommonsHttpOAuthConsumer("", "", SignatureMethod.HMAC_SHA1);
			consumer.setTokenWithSecret(accessToken, accessTokenSecret);
		}
	}

	///////////////////////////////////////////////////////////////////////////
	// Private Methods ////////////////////////////////////////////////////////
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Construct the url from the apiUrl and uri and parameters. The parameter
	 * values are encoded with UTF-8.
	 * 
	 * @param uri the method uri
	 * @param parameters the query parameters
	 */
	private String constructGetUrl(
		String uri, Map<String, String> parameters) 
	{
		StringBuilder url = new StringBuilder(apiUrl);
		url.append(uri);
		
		if(parameters != null) {
			boolean first = true;
			for(Entry<String, String> entry : parameters.entrySet()) {
				String value = entry.getValue();
				if(value == null || value.equals("null")) continue;
				
				try {
					url.append(first ? '?' : '&')
					   .append(entry.getKey())
					   .append('=')
					   .append(URLEncoder.encode(value, "UTF-8"));
				} 
				catch (UnsupportedEncodingException e) {
					url.append(entry.getValue());
				}
				first = false;
			}
		}
		
		return url.toString();
	}
	
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Add string parameters to the post method.
	 * 
	 * @param the method
	 * @param the parameters  
	 */
	private void addPostParameters(
		PostMethod method, Map<String, String> parameters)
	{
		if(parameters != null) {
			for(Entry<String, String> entry : parameters.entrySet()) {
				String value = entry.getValue();
				method.addParameter(entry.getKey(), value);
			}
		}
	}
	
	///////////////////////////////////////////////////////////////////////////

	/**
	 * Execute an HttpMethod, if OAuth credentials have been added then OAuth
	 * authentication headers are sent. 
	 * 
	 * @param method the method to execute
	 * @return the response wrapper
	 */
	private VzaarTransportResponse excuteMethod(HttpMethod method)
		throws OAuthMessageSignerException,	OAuthExpectationFailedException, 
		IOException, HttpException 
	{
		if(consumer != null) {
			consumer.sign(method);
		}
		
		client.executeMethod(client.getHostConfiguration(), method, state);
		
		InputStream in = null;
		
		if(debugOut != null) {
			String body = method.getResponseBodyAsString();
			in = new ByteArrayInputStream(body.getBytes());
			debugOut.write(("<< Response Code = " + 
				method.getStatusCode() + "\n").getBytes());
			debugOut.write(("<< Response Line = " + 
				method.getStatusLine().getReasonPhrase() + "\n").getBytes());
			debugOut.write(("<< Response Body = \n<< " +
				body.replaceAll("\n", "\n<< ") + "\n").getBytes());
		}
		else {
			in = method.getResponseBodyAsStream();
		}
		
		return new VzaarTransportResponse(
			method.getStatusCode(),
			method.getStatusLine().getReasonPhrase(),
			in);
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Add debugging output. Set to null to stop debugging.
	 * 
	 * @param out the output stream to send debug output to.
	 */
	public void setDebugStream(OutputStream out) {
		this.debugOut = out;
	}

	///////////////////////////////////////////////////////////////////////////

	/**
	 * Set the connection factory to use SSLv3 only, i.e. no TLS. This
	 * is for an issue with production servers where SSL is failing. 
	 */
	public void setSsl3Only() {
		Protocol.registerProtocol("https", new Protocol(
			"https", new Ssl3OnlyProtocolFactory(), 443));
	}
	
	///////////////////////////////////////////////////////////////////////////
}
